Java中的IO操作
Java总的来说有三类IO,效率不高,操作简单的BIO(blocking IO),非阻塞的NIO(New IO),和异步非阻塞IO,也就是升级版的NIO(Asynchronous I/O).
IO分类
在学习这三类IO前,需要了解什么是阻塞.什么是异步.两个的含义有什么区别.
同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)所谓同步,就是在发出一个调用*时,在没有得到结果之前,该 *调用 就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。而异步则是相反,调用在发出之后,这个调用就直接返回了,但是没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态来通知调用者。
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会重启线程。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
BIO
BIO过程就如同名字一样,是一个阻塞的IO,服务端通常为每一个客户端都建立一个独立的线程来通过调用accept()来监听客户端消息.如果想处理多个客户端请求则服务端需要建立等同数量的线程来处理这些消息,这就是普遍的一请求一应答的模型.处理完成后返回应答给客户端后销毁线程,因为线程是一个昂贵的资源,这样重复的新建线程,销毁线程,很浪费处理器资源,所以使用BIO同时能够尽可能的少创建线程,就可以用到线程池的方式实现,来达到服务端创建线程数远远小于客户端数的目的,但这种方法只是伪异步IO.
在处理链接数量少的情况下,BIO的效率还不错,并且主要逻辑模型清晰明了,代码简单.但是在上万的链接的情况下,BIO处理起来就非常吃紧了.
NIO
NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。
NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。
NIO特性和NIO与传统IO的区别
- 传统IO(BIO)是一种阻塞IO模型,而NIO是非阻塞的IO模型,区别为当线程读取数据的时候,非阻塞IO可以不用等,而阻塞IO需要一直等待IO完成后才能继续.
- IO面向流,而NIO面向缓冲区.
- 通道(channel) NIO通过通道进行数据读写.通道是双向的,而传统的IO是单向的.通道链接的都是Buffer,所以通道可以异步的读写.
- 选择器(Selectors) NIO拥有选择器,而IO没有.选择器的作用就是用来使用单个线程来处理多个通道(NIO面向buffer,通道只与buffer交互).
AIO
AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。
BIO的流操作
根据上面的IO分类图可以看到IO流按照流的类型可以分为两类,一类是字节流,一类是字符流
两者之间的区别在于
操作单位不同,字节流以字节为单位进行数据传输,而字符流是以字符为单位进行传输
处理元素不同,字节流可处理所有类型数据,但字符流只可以处理以字符类型的数据,也就是说字符流只可处理纯文本数据
输入输出流
输入输出按照字面理解,就是流中的输入和输出
流类型 | 输入 | 输出 |
---|---|---|
字节流 | InputStream | OutputStream |
字符流 | Reader | Writer |
字节流
输入字节流 InputStream
inputStream作为抽象类,必须依靠子类实现具体的操作.在抽象类中定义了如下几个方法:
- 三个重载的read方法,用来读取数据,其中必须在子类中实现抽象的read方法
1 | public abstract int read() throws IOException; |
skip(long n)
方法,用来掉过并丢弃n个字节的数据,并返回被丢弃的数据available()
方法,用来返回输入流中可以读取的字节数,子类需要单独实现该方法,否则会返回0;void close()
方法,子类实现,用来关闭流synchronized void mark(int readlimit)
方法,用来标记输入流的当前位置,同样由子类来具体实现synchronized void reset()
方法,用来返回输入流最后一次调用mark方法的位置
来看看几种不同的InputStream:
FileInputStream
把一个文件作为InputStream,实现对文件的读取操作ByteArrayInputStream
把内存中的一个缓冲区作为InputStream使用StringBufferInputStream
把一个String对象作为InputStreamPipedInputStream
实现了pipe的概念,主要在线程中使用SequenceInputStream
把多个InputStream合并为一个InputStream
输出字节流 outputStream
- OutputStream提供了3个重载的write方法来做数据的输出
1 | // 将参数b中的字节写到输出流 |
public void flush()
将数据缓冲区中数据全部输出,并清空缓冲区。public void close()
关闭输出流并释放与流相关的系统资源。
字符流
字符输入流 Reader
字符输入流和字节流相似,同样定义了read相关方法,但是不同的点在于Reader操作char而不是byte,并且在声明Reader时,将自身作为一个对象,在相关操作上使用synchronized进行同步操作.
1 | protected Reader() { |
同理字符输出流 Writer
如何使用BIO流
- 首先确定是输入还是输出
- 其次确认对象是否为纯文本,如果是纯文本可以选择 字符流的
Reader
和Wirter
,否则需要使用字节流的inputStream
和outputStream
- 然后确定是否要通过流转换来达到增加处理效率的目的,如果需要则使用
InputStreamReader
等进行转换 - 最后,需要确认是否需要使用buffer缓冲来提高效率
- inputStream 字节输入流
1 | public void inputStreamTest(){ |
- outputStream 字节输出流
1 | public void outputStreamTest(){ |
- Reader 字符输入流
1 | public void readerTest(){ |
- Writer 字符输出流
1 | public void writerTest(){ |
NIO操作
在NIO特性一章中提到了NIO是面向Buffer的,双向的数据处理形式。因为NIO分为buffer缓冲区和Channel管道,NIO使用管道操作缓冲区,可以说Channel不与数据打交道,它只负责运输数据。更可以抽象的简单理解为Channel管道为铁路,buffer缓冲区为火车(运载着货物),火车可以去,同样也可以回(双向的)。
Buffer 缓冲区
Buffer是具体的原始类型的容器,Buffer是一个线性的、有限的原始类型元素的集合。Buffer中有三个必要的属性:capacity、limit、position
- capacity:buffer中包含的元素数量
- limit:buffer中的limit是缓冲区里的数据的总数
- position:buffer中position是下一个即将被读写的元素
每个实现子类都需要实现两种方法:get和put,两个是相对的操作,也就是理解为读写数据,每次操作时,都会从buffer中的当前position开始,增长transferred个数量的元素,这里的transferred就是get和put的元素数量。如果使用get操作,超出了limit,那么会出现 BufferUnderflowException ,相反如果使用get超出limit,就会出现 BufferOverflowException,这两种情况下,数据都不会被改变。
Buffer中提供了clear()、 filp()、 rewind()方法用来访问Buffer中的position, limit, 和capacity的值。
- clear() 清空读缓冲区中的内容,之后可以使用put写数据,将limit设置为capacity,将position设置为0
- filp() 切换成读模式,之后可以使用 get 读数据,将limlit设置为当前position,再将position设置为0
- rewind() 可重复读缓冲区的内容
下面通过几个实例来看Buffer相关的方法:
1 | // 分配capacity大小 |
Channel 管道
Channel是IO操作的核心,Channel表示与一些硬件设备,文件,网络等实体的开放连接,能够进行多个不同的IO操作(读与写).
Channel有开关的状态,只要channel创建,则channel的状态就是open的,如果chennel一旦被关闭,那么如果再有后续调用channel的io操作,都会出现异常.(类似jdbc的connection),以防万一,可以调用isopen()方法检测是否被关闭了
再NIO中几个重要的Channel实现类:
- FileChannel: 用于文件的数据读写
- DatagramChannel: 用于UDP的数据读写
- SocketChannel: 用于TCP的数据读写,一般是客户端实现
- ServerSocketChannel: 允许我们监听TCP链接请求,每个请求会创建会一个SocketChannel,一般是服务器实现
用FileChannel来演示创建和传输的过程.
创建channel
有两种方式创建channel,一种是使用file或者fileStream创建cannel,另一种使用FileChannel的静态方法创建
1 | // 1. 使用randomAccessFile 创建channel |
数据读写
使用channel进行数据读写,类似普通的BIO使用buffer.但是需要注意的是,每次都需要将buffer打开读模式,再读,读完后使用clear清空buffer
1 | ByteBuffer readBuffer = ByteBuffer.allocate(1024); |
还可以直接使用通道的transferTo直接复制到另外一个通道中,完成复制
1 | // 使用通道的transferTo |
关闭资源
最后需要手动将channel关掉(必须)
1 | in.close(); |
另外这时候可以使用 try-with-resource 进行简化,整体过程可以参考如下代码:
1 | try ( |